#!/usr/bin/env python
# -*- coding: utf-8 -*-

# Copyright 2010 Mats Ekberg
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#   http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import with_statement
import sys
import os
from time import time, ctime
import cProfile
from optparse import OptionParser
from blobrepo import repository
from blobrepo.sessions import bloblist_fingerprint
from boar_exceptions import *
import client

if sys.version_info >= (2, 6):
    import json
else:
    import simplejson as json

from front import Front
import workdir
from common import *
import settings

def print_help():
    print """Boar commands: 

ci        Commit changes in a work directory
clone     Create or update a clone of a repository
co        Check out files from the repository
diffrepo  Check if two repositories are identical
info      Show some information about the current workdir
import    Import the contents of a folder into your repository
list      Show the contents of a repository or snapshot
locate    Check if some non-versioned files are already present in a repository
mkrepo    Create a new repository
mksession Create a new session
status    List any changes in the current work directory
update    Update the current work directory from the repository
verify    Verify the integrity of the repository

For most commands, you can type "boar <command> --help" to get more
information. The full command reference is available online at
http://code.google.com/p/boar/wiki/CommandReference
"""

def list_sessions(front):
    sessions_count = {}
    for sid in front.get_session_ids():
        session_info = front.get_session_info(sid)
        name = session_info.get("name", "<no name>")
        sessions_count[name] = sessions_count.get(name, 0) + 1
    for name in sessions_count:
        print name, "(" + str(sessions_count[name]) + " revs)"

def list_revisions(front, session_name):
    for sid in front.get_session_ids():
        session_info = front.get_session_info(sid)
        name = session_info.get("name", "<no name>")
        if name != session_name:
            continue
        bloblist = front.get_session_bloblist(sid)
        print "Revision id", str(sid), "(" + session_info['date'] + "),", \
            len(bloblist), "files"

def list_files(front, session_name, revision):
    session_info = front.get_session_info(revision)
    name = session_info.get("name", "<no name>")
    if name != session_name:
        print "There is no such session/revision"
        return
    for info in front.get_session_bloblist(revision):
        print info['filename'], str(info['size']/1024+1) + "k"

def verify_repo(repo, verify_blobs = True):
    front = Front(repo)
    print "Verifying repo", repo
    session_ids = front.get_session_ids()
    print "Verifying %s sessions" % (len(session_ids))
    ok_blobs = set()
    for i in range(0, len(session_ids)):
        id = session_ids[i]
        bloblist = front.get_session_bloblist(id)
        calc_fingerprint = bloblist_fingerprint(bloblist)
        assert calc_fingerprint == front.get_session_property(id, "fingerprint"), \
            "Fingerprint didn't match for session "+str(id)
        for bi in bloblist:
            assert bi['md5sum'] in ok_blobs or \
                front.has_blob(bi['md5sum']), "Session %s is missing blob %s" \
                % (session_ids[i], bi['md5sum'])
            ok_blobs.add(bi['md5sum'])
        print "Snapshot %s (%s): All %s blobs ok" % (id, calc_fingerprint, len(bloblist))
    if not verify_blobs:
        print "Skipping blob verification"
        return True
    print "Collecting a list of all blobs..."
    count = front.init_verify_blobs()
    print "Verifying %s blobs..." % (count)
    done = 0
    while done < count:
        done += len(front.verify_some_blobs())
        print done, "of "+str(count)+" blobs verified, "+ \
            str(round(1.0*done/count * 100,1)) + "% done."
    return True

def cmd_locate(args):
    if len(args) == 0:
        args = ["--help"]
    parser = OptionParser(usage="usage: boar locate <session name>")
    (options, args) = parser.parse_args(args)
    if len(args) == 0:
        raise UserError("You must specify which session to look in.")
    if len(args) > 1:
        raise UserError("Too many arguments.")
    sessionName = args[0]

    root = os.getcwd().decode(sys.getfilesystemencoding())
    tree = get_tree(root)
    tree.sort()
    front = init_repo_from_env(cmdline_repo)
    wd = workdir.Workdir(front.get_repo_path(), sessionName, "", None, root)
    missing = []
    found = 0
    for f in tree:
        csum = md5sum_file(f)
        session_filenames = list(wd.get_filesnames(csum))
        session_dirs = [os.path.dirname(fn) for fn in session_filenames]
        if not session_filenames:
            print "Missing:", f
            missing.append(f)
            continue
        if session_filenames:
            print "OK:", f
            found += 1
            for p in session_filenames:
                print "   " + p
    print "%s files exists in the given session, %s do not." % (found, len(missing))
    for f in missing:
        print "Missing:", f

def cmd_status(args):
    parser = OptionParser(usage="usage: boar status [options]")
    parser.add_option("-v", "--verbose", dest = "verbose", action="store_true",
                      help="Show information about unchanged files as well")
    (options, args) = parser.parse_args(args)
    wd = workdir.init_workdir(os.getcwd())
    if not wd:
        raise UserError("No workdir found here.")
    unchanged_files, new_files, modified_files, deleted_files, ignored_files \
        = wd.get_changes()
    filestats = {}
    def in_session(f):
        f_wd = strip_path_offset(wd.offset, f)
        return "S" if wd.exists_in_session(wd.cached_md5sum(f_wd)) else " "
    def in_workdir(f):
        csum = wd.get_blobinfo(f)['md5sum']
        return "W" if wd.exists_in_workdir(csum) else " "

    for f in new_files:
        filestats[f] = "A" + in_session(f)
    for f in modified_files:
        filestats[f] = "M" + in_workdir(f)
    for f in deleted_files:
        filestats[f] = "D" + in_workdir(f)
    if options.verbose:
        for f in unchanged_files:
            filestats[f] = " "
        for f in ignored_files:
            filestats[f] = "i"
    filenames = filestats.keys()
    filenames.sort()
    for f in filenames:
        print filestats[f], f

def cmd_info(args):
    parser = OptionParser(usage="usage: boar info")
    (options, args) = parser.parse_args(args)
    if len(args) != 0:
        raise UserError("Info command does not accept any arguments.")

    wd = workdir.init_workdir(os.getcwd())
    if wd:
        print "Using a work directory:"
        print "   Workdir root:", wd.root
        print "   Repository:", wd.repoUrl
        print "   Session:", wd.sessionName, "/", wd.offset
        print "   Revision:", wd.revision
        
    #env_front = init_repo_from_env()

def cmd_mkrepo(args):
    if len(args) == 0:
        args = ["--help"]
    parser = OptionParser(usage="usage: boar mkrepo <new repo path>")
    (options, args) = parser.parse_args(args)
    repository.create_repository(args[0])


def cmd_list(args):
    parser = OptionParser(usage="usage: boar list [session name [snapshot id]]")
    (options, args) = parser.parse_args(args)
    if len(args) > 2:
        raise UserError("Too many arguments")
    front = init_repo_from_env(cmdline_repo)
    if len(args) == 0:
        list_sessions(front)
    elif len(args) == 1:
        list_revisions(front, args[0])
    elif len(args) == 2:
        list_files(front, args[0], args[1])
    else:
        print "Duuuh?"

def cmd_verify(args):
    parser = OptionParser(usage="usage: boar verify [options]")
    parser.add_option("-q", "--quick", dest = "quick", action="store_true",
                      help="Only check that the repository looks reasonably ok (skip blob checksumming)")
    (options, args) = parser.parse_args(args)
    front = init_repo_from_env(cmdline_repo)
    verify_repo(front.repo, verify_blobs = not options.quick)

def cmd_import(args):
    parser = OptionParser(usage="usage: boar import [options] <folder to import> <session name>[/path/]")
    parser.add_option("-v", "--verbose", dest = "verbose", action="store_true",
                      help="Show more details about what is happening.")
    parser.add_option("-n", "--dry-run", dest = "dry_run", action="store_true",
                      help="Don't actually do anything. Just show what will happen.")
    parser.add_option("-w", "--create-workdir", dest = "create_workdir", action="store_true",
                      help="Turn the imported directory into a workdir.")
    base_session = None
    (options, args) = parser.parse_args(args)
    assert len(args) <= 2
    path_to_ci = os.path.abspath(args[0]).decode()
    session_name = os.path.basename(args[0]) #TODO: what happens when giving only one arg?
    session_offset = ""
    if len(args) > 1:
        if "/" in args[1]:
            # TODO: this won't work so well with windows paths
            session_name, session_offset = args[1].split("/", 1)
        else:
            session_name = args[1]
    if options.verbose:
        print "Session name:", session_name, "Session offset:", session_offset
    if not os.path.exists(path_to_ci):
        raise UserError("Path to check in does not exist: " + path_to_ci)
    front = init_repo_from_env(cmdline_repo)
    if not front.find_last_revision(session_name):
        raise UserError("No session with the name '%s' exists." % (session_name))
    wd = workdir.Workdir(front.get_repo_path(), session_name, session_offset, None, path_to_ci)
    if options.verbose:
        wd.setLogOutput(sys.stdout)
    session_id = wd.checkin(write_meta = options.create_workdir, 
                            fail_on_modifications = True, add_only = True, dry_run = options.dry_run)
    print "Checked in session id", session_id

def cmd_update(args):
    parser = OptionParser(usage="usage: boar update [options]")
    parser.add_option("-r", "--revision", action="store", dest = "revision", type="int", 
                      help="The revision to update to (defaults to latest)")
    parser.add_option("-i", "--ignore-errors", action="store_true", dest = "ignore_errors", 
                      help="Do not abort the update if there are errors while writing.")
    (options, args) = parser.parse_args(args)
    if len(args) != 0:
        raise UserError("Update does not accept any non-option arguments")
    wd = workdir.init_workdir(os.getcwd())
    wd.update(new_revision = options.revision, ignore_errors = options.ignore_errors)

def cmd_ci(args):
    parser = OptionParser(usage="usage: boar ci [options]")
    parser.add_option("-a", "--add-only", dest = "addonly", action="store_true",
                      help="Only new files will be committed. Modified and deleted files will be ignored.")
    (options, args) = parser.parse_args(args)
    if args:
        raise UserError("Unexpected arguments: "+str(args))
    wd = workdir.init_workdir(os.getcwd())
    session_id = wd.checkin(add_only = options.addonly)
    print "Checked in session id", session_id

def cmd_mksession(args):
    if len(args) == 0:
        args = ["--help"]
    parser = OptionParser(usage="usage: boar mksession <new session name>")
    (options, args) = parser.parse_args(args)
    if len(args) != 1:
        raise UserError("mksession requires a single valid session name as argument")
    session_name, = args
    front = init_repo_from_env(cmdline_repo)
    if front.find_last_revision(session_name) != None:
        raise UserError("There already exists a session named '%s'" % (session_name))
    front.mksession(session_name)
    print "New session '%s' was created successfully" % (session_name)

def cmd_co(args): 
    parser = OptionParser(usage="usage: boar co [options] <session name>[/path/] [workdir name]")
    parser.add_option("-r", "--revision", action="store", dest = "revision", type="int", 
                      help="The revision to check out (defaults to latest)")
    (options, args) = parser.parse_args(args)
    if not args:
        raise UserError("You must specify a session name with an optional subpath (i.e 'MyPictures/summer2010')")    
    if len(args) > 2:
        raise UserError("Too many arguments")
    session_name, throwaway, offset = args.pop(0).partition("/")
    workdir_path = os.path.abspath(session_name).decode()
    if args:
        workdir_path = os.path.abspath(args.pop(0)).decode()
    if os.path.exists(workdir_path):
        raise UserError("Workdir path '%s' already exists" % (workdir_path))
    assert not args
    # Args parsing complete

    front = init_repo_from_env(cmdline_repo)
    if options.revision:
        sid = front.find_last_revision(session_name, [options.revision])
        if not sid:
            raise UserError("No such revision or session found")
    else:
        sid = front.find_last_revision(session_name)
        if not sid:
            raise UserError("No such session found: %s" % (session_name))

    print "Checking out to workdir", workdir_path

    os.mkdir(workdir_path)
    wd = workdir.Workdir(front.get_repo_path(), session_name, offset, sid, workdir_path)
    wd.checkout()

def cmd_find(front, args):
    cs, sessionName = args
    sessionId = front.find_last_revision(sessionName)
    for bi in front.get_session_bloblist(sessionId):
        if bi['md5sum'] == cs:
            print bi['filename']

def cmd_clone(args):
    if len(args) == 0:
        args = ["--help"]
    parser = OptionParser(usage="usage: boar clone <source repo> <destination repo>")
    (options, args) = parser.parse_args(args)
    if len(args) != 2:
        raise UserError("You must specify one source repository and one destination repository.")
    repopath1, repopath2 = args
    if repopath1.startswith("boar://") or repopath2.startswith("boar://"):
        raise UserError("Cloning requires local repositories")
    repopath1 = os.path.abspath(repopath1)
    repopath2 = os.path.abspath(repopath2)
    repo1 = repository.Repo(repopath1)
    if not os.path.exists(repopath2):
        repository.create_repository(repopath2)
    repo2 = repository.Repo(repopath2)
    if repo1.isIdentical(repo2):
        print "Repositories are already identical"
        return
    print "Quick verifying source repo"
    verify_repo(repo1, verify_blobs = False)
    print "Quick verifying destination repo"
    verify_repo(repo2, verify_blobs = False)
    repo2.pullFrom(repo1)
    print "Performing full verify on cloned repo"
    verify_repo(repo2, verify_blobs = True)

def cmd_diffrepo(args):
    if len(args) == 0:
        args = ["--help"]
    parser = OptionParser(usage="usage: boar diffrepo <repo 1> <repo 2>")
    (options, args) = parser.parse_args(args)
    if len(args) != 2:
        raise UserError("You must specify exactly two existing repositories.")
    repopath1, repopath2 = args
    if repopath1.startswith("boar://") or repopath2.startswith("boar://"):
        raise UserError("Cloning requires local repositories")
    repopath1 = os.path.abspath(repopath1)
    repopath2 = os.path.abspath(repopath2)
    repo1 = repository.Repo(repopath1)
    repo2 = repository.Repo(repopath2)
    if repo1.isIdentical(repo2):
        assert repo2.isIdentical(repo1)
        print "Repositories are identical"
        return_code = 0
    else:
        print "Repositories differ"
        return_code = 1
    return return_code

def cmd_export_md5(wd, args):
    wd.export_md5()

def init_repo_from_env(repo_from_cmdline):
    repopath = os.getenv("REPO_PATH")
    if repo_from_cmdline:
        repopath = repo_from_cmdline
    front = None
    msg = None
    if not repopath:
        raise UserError("You need to specify a repository to operate on. "+\
                            "Use the --repo option or set $REPO_PATH.")
    elif repopath.startswith("boar://"):
        front = client.connect(repopath)
    else:
        repopath = os.path.abspath(repopath)
        if not os.path.exists(repopath):
            raise UserError("Provided repository path does not exist: "+repopath)
        front = Front(repository.Repo(repopath))
    assert front
    return front

def main():
    if len(sys.argv) <= 1:
        print_help()
        return 1

    args = sys.argv[1:]
    global cmdline_repo
    cmdline_repo = None
    for i in range(0, len(args)):
        # This is ridiculous, but I just can't get OptParse to just
        # look for --repo without exploding on other "unknown options".
        # TODO: make less silly
        if args[i] == "--repo":
            args.pop(i)
            try:
                cmdline_repo = args.pop(i) 
                break
            except:
                raise UserError("You must specify a valid repository after --repo")
        if args[i].startswith("--repo="):
            _, cmdline_repo = args.pop(i).split("=")
            break

    if args[0] == "mkrepo":
        return cmd_mkrepo(args[1:])
    elif args[0] == "import":
        return cmd_import(args[1:])
    elif args[0] == "list":        
        return cmd_list(args[1:])
    elif args[0] == "verify":
        return cmd_verify(args[1:])
    elif args[0] == "co":
        return cmd_co(args[1:])
    elif args[0] == "status":
        return cmd_status(args[1:])
    elif args[0] == "info":
        return cmd_info(args[1:])
    elif args[0] == "ci":
        return cmd_ci(args[1:])
    elif args[0] == "update":
        return cmd_update(args[1:])
    elif args[0] == "find":
        front = init_repo_from_env(cmdline_repo)
        return cmd_find(front, args[1:])
    elif args[0] == "locate":
        return cmd_locate(args[1:])
    elif args[0] == "mksession":
        return cmd_mksession(args[1:])
    elif args[0] == "exportmd5":
        wd = workdir.init_workdir(os.getcwd())
        return cmd_export_md5(wd, args[1:])
    elif args[0] == "clone":
        return cmd_clone(args[1:])
    elif args[0] == "diffrepo":
        return cmd_diffrepo(args[1:])
    else:
        print_help()
        return 1


return_code = 0
if __name__ == "__main__":
    t1 = time()
    sys.stdout = StreamEncoder(sys.stdout)
    #cProfile.run('main()', "prof.txt")
    #import pstats
    #p = pstats.Stats('prof.txt')
    #p.sort_stats('cum').print_stats(10)
    try:
        return_code = main()
    except UserError as e:
        print "ERROR:", e.value
        return_code = 1
    except repository.MisuseError as e:
        print "REPO USAGE ERROR:", e
        return_code = 1
    except repository.CorruptionError as e:
        print "REPO CORRUPTION:", e
        return_code = 1
 
    t2 = time()
    print "Finished in", round(t2-t1, 2), "seconds"
    sys.exit(return_code)
